Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

crypto crate implementation #17

Merged
merged 3 commits into from
Nov 27, 2023
Merged

crypto crate implementation #17

merged 3 commits into from
Nov 27, 2023

Conversation

amika-sq
Copy link
Contributor

@amika-sq amika-sq commented Nov 21, 2023

This PR is a first pass at an implementation for the crypto crate.

This implementation utilized the ssi_jwk and ssi_jws crates from Spruce's excellent SSI library: https://github.com/spruceid/ssi/

The structure & operations here follows that in web5-kt/crypto with a few notable exceptions, which I have outlined below. I am open to changing/renaming any of these!

PrivateKey and PublicKey

There is an additional abstraction over JWKs: PrivateKey and PublicKey. This allows us to move signing / verifying closer to the actual objects doing the work.

Spruce's ssi library utilizes JWKs as the key type for most of it's operations, so these abstractions directly wrap a JWK. In the future, if we want to introduce another key type, we can convert these into generic traits.

This abstraction also helps with the uniffi-rs binding generation process which we will be using for Swift, which will be coming in a later PR. Because we don't own the JWK type (it's defined in ssi_jwk), we can't directly expose it to the binding generator without duplicating it. Using Rust newtypes that wrap the JWK allows us to get around this.

LocalKeyManager and KeyStore

Our ultimate goal here is to export this Rust code into Swift code. On Swift, we want a way to generate cryptographic keys via Rust, but then store them using secure storage mechanisms such as the Keychain and the Secure Enclave. In order to accomodate this, I needed to split apart the "key generation" and "key storage" tasks into separate traits.

LocalKeyManager is how I did this. It's responsibilities are to generate the cryptographic keys locally (locally meaning in Rust) and then delegate the actual storage to a KeyStore trait.

I have provided a InMemoryKeyStore implementation that stores the cryptographic keys in memory. But anyone can provide their own implementation, including foreign languages.

.get(key_alias)?
.ok_or(KeyManagerError::SigningKeyNotFound)?;

let signed_payload = private_key.sign(&payload.to_vec())?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it assumed that all key types expose this sign method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct. The PrivateKey struct is currently responsible for all key types, which itself is a thin wrapper around a JWK.

Definitely flexible here to alternative approaches!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it may be worth including a signing algorithm parameter, as sometimes the same key can be used with different algorithms (e.g. secp256k1 can be used with ECDH and ECDSA)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should still be possible, by the caller setting the algorithm field on the JWK.

Currently, the algorithm is computed by this function in Spruce's ssi-jwk crate:
https://github.com/spruceid/ssi/blob/main/ssi-jwk/src/lib.rs#L341

This logic matches our signer lookup that's currently being done in Kotlin here:
https://github.com/TBD54566975/web5-kt/blob/main/crypto/src/main/kotlin/web5/sdk/crypto/Crypto.kt#L111C22-L111C22

Copy link
Contributor

@andresuribe87 andresuribe87 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot to hit send a bit back...

/// is being run. Key storage is provided by a [`KeyStore`] trait implementation, allowing the keys
/// to be stored wherever is most appropriate for the application.
pub struct LocalKeyManager {
key_store: Arc<dyn KeyStore>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand the ownership story here for KeyStore and LocalKeyManager, would you mind elaborating on how it's supposed to work?

Tangentially, for my learning, do you know when you are expected to use Arc::clone?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arc is a shared data reference that uses the concept of reference counting. When you create an Arc via Arc::new() around some piece of data, that data is placed on the heap along with two counters for "strong" and "weak" references to it. After Arc::new(), it looks something like this:

Untitled scene

The Arc you get back is actually a pointer to some heap allocated memory (called ArcInner), which contains the counts, as well as the actual shared data itself. If the Arc pointer you get back goes out of scope and gets deallocated, the strong reference counter that the ArcInner was pointing to is atomically decremented. When the strong count falls to zero, the heap allocated memory that contains the actual data is then cleaned up.

Now looking at these particular lines:

pub struct LocalKeyManager {
    key_store: Arc<dyn KeyStore>,
}

What this is saying is that LocalKeyManager owns an Arc, which points to a KeyStore. The lifetime of the Arc will only end when the LocalKeyManager's lifetime ends. This means that the strong reference count will remain non-zero for the duration of LocalKeyManager's lifetime, guaranteeing that it's always available to use.

In order to construct a LocalKeyManager, the caller must to provide an Arc<dyn Keystore> value in the constructor. This can be done either by creating a new Arc like I mentioned earlier with Arc::new(), or the caller can clone an existing Arc.

Cloning an Arc isn't actually cloning the heap allocated data. Instead, it's cloning the pointer to that heap allocated data. As a part of the clone operation, the strong reference count is atomically incremented. So, even if the originally cloned Arc goes out of scope and gets deallocated, decrementing the reference count, the reference count is still non-zero.


I hope that made some sort of sense 😅 . Happy to chat through this more if you'd like more clarity, or had additional questions! Arc is definitely one of the more interesting data types in Rust, and it's heavily used throughout the bindings generation process which will come in future PRs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Asked more in the line below.

Copy link
Member

@mistermoe mistermoe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🏅

impl LocalKeyManager {
/// Constructs a new `LocalKeyManager` that stores keys in the provided `KeyStore`.
pub fn new(key_store: Arc<dyn KeyStore>) -> Self {
Self { key_store }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a place where you should be using ::clone on the passed in key_store? If not, mind elaborating why?

Copy link
Contributor Author

@amika-sq amika-sq Nov 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, not exactly. The contract here says "I need an Arc<dyn KeyStore>." It's the caller's responsibility to provide that. Whether that's done by a ::clone(), ::new(), or moving an existing Arc is an implementation detail that the caller must handle. If they do not, it's a compilation error.

@@ -0,0 +1,7 @@
mod key_manager;
pub use key_manager::*;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels like wildcard is difficult to maintain. I typically recommend being explicit on what this is exposing.

Tangentially, would it make sense to setup a linter? I briefly looked online and found https://github.com/rust-lang/rust-clippy

Copy link
Contributor Author

@amika-sq amika-sq Nov 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty common in Rust library development. It's just saying that I can use the key_manager module without specifying key_manager again.

Without this line, caller's use statements would look like this:
use crypto::key_manager::key_manager::KeyManager

But with this line, callers can reference it like so:
use crypto::key_manager::KeyManager

/// is being run. Key storage is provided by a [`KeyStore`] trait implementation, allowing the keys
/// to be stored wherever is most appropriate for the application.
pub struct LocalKeyManager {
key_store: Arc<dyn KeyStore>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Asked more in the line below.

[package]
name = "crypto"
version = "0.1.0"
edition = "2021"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2023?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every once in a while, there's a change to the Rust language that isn't backwards compatible. Editions are how that's conveyed in the Cargo.toml file. 2021 was the latest change to language, and we are just declaring that we're compatible with versions from that point on.

More info on editions here! https://doc.rust-lang.org/edition-guide/editions/

Copy link
Contributor

@KendallWeihe KendallWeihe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! This is cool. I'm a total Rust noob so don't feel qualified to give an official ✅

/// Implementations of this trait might provide key management through various Key Management
/// Systems (KMS), such as AWS KMS, Google Cloud KMD, Hardware Security Modules (HSM), or simple
/// in-memory storage, each adhering to the same consistent API for usage within applications.
pub trait KeyManager: Send + Sync {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i noticed you have : Send + Sync bits here, and i think these traits have to do with being able to pass types across threads, and share references between threads.

i don't know enough about rust to fully understand the docs but could you tell me why you chose to add these two traits? is it because we need to use Arc in all key manager's implementations?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent question! It's actually to do with exposing this trait for external bindings via uniffi (which will be upcoming in a future PR). There's a requirement that exposed traits MUST implement Send + Sync, because which thread it comes across isn't guaranteed. There's a bit of explanation about it here: https://mozilla.github.io/uniffi-rs/udl/interfaces.html#exposing-traits-as-interfaces

So, for now, it's mostly for things yet to come!

Copy link

@jiyoonie9 jiyoonie9 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i learned a lot by just reading this PR and the comments! thanks for writing this 😊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants